#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
import json
import os
import re
import stat
import subprocess
import sys
import time
from datetime import datetime
from threading import Timer


class Constants:
    # result code
    SUCCESS_CODE = 0
    INVALID_CONF_CODE = 1
    INVALID_USER_CODE = 2

    # constants related to local license configuration
    CONFIG_TEXT_ENCODING = 'utf-8'
    ROOT_UID = 0

    # constants of FlexNet
    CMD_TIME_OUT = 10  # unit:seconds
    FLEX_LIC_SERVER_HOST_REGEX = r'License server status: ([\w,.@\-]+)'
    FLEX_LIC_SERVER_STATUS_REGEX = r'([\w\.\-]+): license server (\w+)( \((MASTER)\))? (v[\d\.]+)'
    FLEX_LIC_USED_REG = r'Users of (\w+):  \(Total of (\d+) license[s]? issued;  Total of (\d+) license[s]? in use\)'
    FLEX_LIC_VENDOR_REGEX = r'(.+): (\w+) v(.+)'
    FLEX_VENDOR_DAEMON_PREFIX = "Vendor daemon status"
    FLEX_SERVER_MATCH_GROUP_LEN = 2
    FLEX_PORT_REG_INDEX = 0
    FLEX_HOST_REG_INDEX = 1
    FLEX_FEATURE_MATCH_GROUP_LEN = 3
    FLEX_FEATURE_REG_INDEX = 0
    FLEX_TOTAL_REG_INDEX = 1
    FLEX_USED_REG_INDEX = 2
    LIC_FILE_REGEX = r'^(\d+)@([\w.\-]+)$'
    LINE_SEP = '\n'
    HOST_SEP = ','
    HOST_PORT_CONCAT_SYMBOL = '@'

    # status
    UP = 'UP'
    DOWN = 'DOWN'

    # common constants
    ALLOWED_MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024  # 1GB, unit:Bytes
    MAX_LICENSE_SIZE = 50000
    MAX_COUNT_VALUE = 2147483647


SAMPLE_TIME = datetime.now().strftime('%Y-%m-%d %H:%M:00')
LICENSE_MANAGER_SERVER = os.getenv('LICENSE_MANAGER_SERVER')
FLEXNET_LMUTIL_PATH = os.getenv('FLEXNET_LMUTIL_PATH')
MANAGER_NAME = os.getenv('MANAGER_NAME')
CLUSTER_NAME = os.getenv('CLUSTER_NAME')
RESPONSE_TIME = 0


class FlexNetService:
    def __init__(self, service_path):
        self.sample_time = SAMPLE_TIME
        self.service_path = service_path
        self.manager_name = MANAGER_NAME
        self.cluster_name = CLUSTER_NAME
        self.type = 'flexnet'
        self.master = None
        self.version = None
        self.status = 'DOWN'
        self.quorum_status = None
        self.access_successes = 0
        self.access_failures = 1
        self.response_time = None
        self.avg_response_time = None
        self.max_response_time = None
        self.total_features = None
        self.in_use_features = None
        self.total_feature_licenses = None
        self.in_use_feature_licenses = None

    def set_service_info(self, status, master, version):
        if status.upper() == Constants.UP:
            self.status = Constants.UP
            self.response_time = self.avg_response_time = self.max_response_time = RESPONSE_TIME
            self.access_successes = 1
            self.access_failures = 0
        else:
            self.status = Constants.DOWN
            self.access_successes = 0
            self.access_failures = 1

        self.master = 'MASTER' if master.upper() == 'MASTER' else None
        self.version = version
        self.quorum_status = status


class FlexNetParseError(Exception):
    pass


def main():
    if os.geteuid() == Constants.ROOT_UID:
        print_output(generate_default_output_data(Constants.INVALID_CONF_CODE,
                                                  'Error: root user is not allowed to execute.'))
        sys.exit(Constants.INVALID_USER_CODE)
    sample_flexnet_licenses()


def generate_default_output_data(code, err_msg):
    output_data = OutputData()
    output_data.result_code = code
    output_data.err_msg = err_msg
    for service_name in LICENSE_MANAGER_SERVER.split(Constants.HOST_SEP):
        flex_service = FlexNetService(service_name)
        output_data.license_service_list.append(flex_service)
    return output_data


def sample_flexnet_licenses():
    license_sampler = FlexNetLicenseSampler(FLEXNET_LMUTIL_PATH, LICENSE_MANAGER_SERVER)
    license_sampler.sample_license()


class FlexNetLicenseSampler:
    """ License Sampler for FlexNet licenses"""

    def __init__(self, lm_util_path, lm_license_file):
        self.__lm_util_path = lm_util_path
        self.__lm_license_file = lm_license_file

    def sample_license(self):
        """ print OutputData in json style"""
        output_data = self._get_license_output_data()
        if len(output_data.license_service_list) > Constants.MAX_LICENSE_SIZE:
            output_data.license_service_list = output_data.license_service_list[:Constants.MAX_LICENSE_SIZE]

        print_output(output_data)

    def _get_license_output_data(self):
        output_data = OutputData()
        if not is_valid_path(self.__lm_util_path):
            result_code = Constants.INVALID_CONF_CODE
            err_msg = "lmutilPath:{} is not valid.".format(self.__lm_util_path)
            return generate_default_output_data(result_code, err_msg)
        if not self.__has_valid_license_file():
            result_code = Constants.INVALID_CONF_CODE
            err_msg = "license file format is invalid:{}.".format(self.__lm_license_file)
            return generate_default_output_data(result_code, err_msg)

        # 计算服务状态信息，集群中没有服务状态是UP时，直接返回，终止features信息的查询
        lmstat_service_result = self.__get_service_status_result(self.__lm_util_path, self.__lm_license_file)
        if not lmstat_service_result.returncode == Constants.SUCCESS_CODE:
            result_code = lmstat_service_result.returncode
            err_msg = "cmd execute failed. reason:{}".format(
                lmstat_service_result.stderr.read().decode(Constants.CONFIG_TEXT_ENCODING))
            return generate_default_output_data(result_code, err_msg)
        lmstat_a_output_lines = lmstat_service_result.stdout.readlines()
        try:
            feature_service_list = self._parse_flexnet_output(lmstat_a_output_lines)
        except FlexNetParseError as err:
            result_code = Constants.INVALID_CONF_CODE
            err_msg = err
            return generate_default_output_data(result_code, err_msg)
        else:
            if all([service.status != Constants.UP for service in feature_service_list]):
                output_data.license_service_list.extend(feature_service_list)
                return output_data

        # 计算features信息
        lmstat_a_result = self.__get_lmstat_a_result(self.__lm_util_path, self.__lm_license_file)
        if not lmstat_a_result.returncode == Constants.SUCCESS_CODE:
            result_code = lmstat_a_result.returncode
            err_msg = "cmd execute failed. reason:{}".format(
                lmstat_a_result.stderr.read().decode(Constants.CONFIG_TEXT_ENCODING))
            return generate_default_output_data(result_code, err_msg)
        # get result
        lmstat_a_output_lines = lmstat_a_result.stdout.readlines()
        try:
            feature_service_list = self._parse_flexnet_output(lmstat_a_output_lines)
        except FlexNetParseError as err:
            result_code = Constants.INVALID_CONF_CODE
            err_msg = err
            output_data = generate_default_output_data(result_code, err_msg)
        else:
            output_data.license_service_list.extend(feature_service_list)
        return output_data

    def __has_valid_license_file(self):
        if self.__lm_license_file is None or len(self.__lm_license_file) == 0:
            return False
        port_server_array = str.strip(self.__lm_license_file).split(Constants.HOST_SEP)
        re_obj = re.compile(Constants.LIC_FILE_REGEX)
        for port_server in port_server_array:
            match_result = re_obj.match(port_server)
            if match_result is None:
                return False
        return True

    @staticmethod
    def __get_lmstat_a_result(lm_util_path, lm_license_file):
        """ query license data from FlexNet
            flexnet command example:
            ./lmutil lmstat -c port@server:port1@server1 -t <timeout_value>
            return lmstat -a command result
        """
        lmstat_cmd = "{} lmstat -a -c {} -t {}".format(lm_util_path,
                                                       lm_license_file,
                                                       Constants.CMD_TIME_OUT)
        lmstat_result = run_cmd(lmstat_cmd)
        return lmstat_result

    @staticmethod
    def __get_service_status_result(lm_util_path, lm_license_file):
        """ query license data from FlexNet
            flexnet command example:
            ./lmutil lmstat -c port@server:port1@server1 -t <timeout_value>
            return lmstat -a command result
        """
        lmstat_cmd = "{} lmstat -lm -c {} -t {}".format(lm_util_path,
                                                        lm_license_file,
                                                        Constants.CMD_TIME_OUT)
        start_time = time.time()
        lmstat_result = run_cmd(lmstat_cmd)
        global RESPONSE_TIME
        RESPONSE_TIME = int((time.time() - start_time) * 1000)
        return lmstat_result

    def _parse_flexnet_output(self, output_lines):
        server_host_list = []
        self.__parse_lmstat_a_output(output_lines, server_host_list)
        return server_host_list

    def __parse_lmstat_a_output(self, output_lines, server_host_list):
        if len(output_lines) == 0:
            return
        reg_feature_obj = re.compile(Constants.FLEX_LIC_USED_REG)
        reg_server_host_obj = re.compile(Constants.FLEX_LIC_SERVER_HOST_REGEX)
        reg_server_status_obj = re.compile(Constants.FLEX_LIC_SERVER_STATUS_REGEX)
        need_parse_server_host = True
        need_parse_server_status = True
        features_summary_map = {
            'total_features': 0,
            'in_use_features': 0,
            'total_feature_licenses': 0,
            'in_use_feature_licenses': 0
        }
        for line in output_lines:
            line = line.decode(Constants.CONFIG_TEXT_ENCODING).strip()
            if len(line) == 0:
                continue
            if need_parse_server_host:
                try:
                    self.__parse_flexnet_server_host(line, reg_server_host_obj, server_host_list)
                except FlexNetParseError:
                    continue
                else:
                    if not server_host_list:
                        raise FlexNetParseError('Server host info is empty.')
                    need_parse_server_host = False
            else:
                if need_parse_server_status:
                    self.__parse_flexnet_server_status(line, reg_server_status_obj, server_host_list)

                if line.startswith(Constants.FLEX_VENDOR_DAEMON_PREFIX):
                    # 匹配到FLEX_VENDOR_DAEMON_PREFIX信息，表明服务状态信息已解析完成
                    need_parse_server_status = False
                    continue
                try:
                    feature_name, total_count, used_count = self.__parse_flexnet_feature(line, reg_feature_obj)
                except FlexNetParseError:
                    continue
                features_summary_map['total_features'] += 1
                if used_count > 0:
                    features_summary_map['in_use_features'] += 1
                features_summary_map['total_feature_licenses'] += total_count
                features_summary_map['in_use_feature_licenses'] += used_count
        for key, value in features_summary_map.items():
            for flexnet_service in server_host_list:
                if flexnet_service.status == Constants.UP:
                    setattr(flexnet_service, key, value)

    @staticmethod
    def __parse_flexnet_feature(output_line, regex_obj):
        match_result = regex_obj.match(output_line)
        if match_result is None:
            raise FlexNetParseError()
        regex_result = match_result.groups()
        if len(regex_result) < Constants.FLEX_FEATURE_MATCH_GROUP_LEN:
            raise FlexNetParseError()
        feature_name = regex_result[Constants.FLEX_FEATURE_REG_INDEX]
        total_count = regex_result[Constants.FLEX_TOTAL_REG_INDEX]
        used_count = regex_result[Constants.FLEX_USED_REG_INDEX]
        if not (total_count.isdigit() and used_count.isdigit()):
            raise FlexNetParseError()
        return feature_name, int(total_count), int(used_count)

    def __parse_flexnet_server_host(self, output_line, reg_server_host_obj, server_host_list):
        if len(output_line) == 0:
            raise FlexNetParseError()
        match_result = reg_server_host_obj.match(output_line)
        if match_result is None:
            raise FlexNetParseError()
        for host in match_result.group(1).strip().split(Constants.HOST_SEP):
            format_host = self.__format_host(host)
            if format_host:
                server_host_list.append(FlexNetService(format_host))

    @staticmethod
    def __format_host(host):
        re_obj = re.compile(Constants.LIC_FILE_REGEX)
        match_result = re_obj.match(host)
        if match_result is None:
            return ''
        return host

    @staticmethod
    def __parse_flexnet_server_status(output_line, reg_server_status_obj, server_host_list):
        if len(output_line) == 0:
            return
        match_result = reg_server_status_obj.match(output_line)
        if match_result is None:
            return

        match_tuple = match_result.groups()
        for value in server_host_list:
            if value.service_path.endswith(Constants.HOST_PORT_CONCAT_SYMBOL + match_tuple[0]):
                value.set_service_info(match_tuple[1], match_tuple[3], match_tuple[4])
                break


# common function
def run_cmd(cmd_string):
    # better to use shlex.split(cmd_string)
    cmd_array = re.split(" ", cmd_string)
    cmd_proc = subprocess.Popen(cmd_array,
                                shell=False,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
    timer = Timer(Constants.CMD_TIME_OUT, cmd_proc.kill)
    try:
        timer.start()
        cmd_proc.wait()
    finally:
        timer.cancel()
    return cmd_proc


def is_valid_path(path):
    """
       Check whether the path is valid.
       check points:
       1. file exists
       2. file access
       3. file size
       4. file mode
    """
    if path is None or len(path) == 0:
        return False
    if not os.path.exists(path):
        return False
    if not os.access(path, os.R_OK):
        return False
    stat_result = os.stat(path)
    if stat_result.st_size > Constants.ALLOWED_MAX_FILE_SIZE:
        return False
    if not stat.S_ISREG(stat_result.st_mode):
        return False
    return True


def print_output(output_data):
    print(json.dumps(obj=output_data, default=lambda x: x.__dict__,
                     sort_keys=False, indent=2))


class LicenseFeature:
    """ Feature License Class
    """

    def __init__(self, feature_name, feature_count, used_count, is_remote):
        self.feature_name = feature_name
        self.total_count = feature_count
        self.used_count = used_count
        self.vendor = ""
        self.version = ""
        self.server_host = ""
        self.server_port = -1
        self.is_remote = is_remote


class OutputData:
    """ script output struct"""

    def __init__(self):
        self.result_code = Constants.SUCCESS_CODE
        self.err_msg = ""
        self.license_service_list = []


if __name__ == '__main__':
    main()
